Skip to content

Accept an expected consumption of zero - #61

Merged
Alex-GF merged 2 commits into
isa-group:developfrom
javiercavlop:fix/zero-expected-consumption
Jul 31, 2026
Merged

Accept an expected consumption of zero#61
Alex-GF merged 2 commits into
isa-group:developfrom
javiercavlop:fix/zero-expected-consumption

Conversation

@javiercavlop

Copy link
Copy Markdown
Contributor

The problem

expectedConsumption cannot express "this limit takes part in the evaluation, but this call spends nothing of it".

When a caller provides expectedConsumption, evaluateFeature requires a value for every limit involved in the feature's expression, or it refuses the call:

Please note that if you provide an expectedConsumption for any limit, you must provide it for all limits involved in that feature's evaluation.

So the only value a caller can give for a limit they are not spending is 0. Two falsy checks in featureEvaluation.ts rejected exactly that, and reported it with the message meant for a caller who forgot the limit entirely.

The second check is the wider bug: it tests the sum, not the input, so it also refuses an ordinary positive consumption whenever the current usage level is still 0 — the state of every contract on its first call, and of every renewable limit right after it resets.

// _buildSuccessResult
const updatedUsageLevel = _updateUsageLevel(subscriptionContext[limitKey], expectedConsumption[limitKey]);
if (!updatedUsageLevel) {          // 0 + 0 === 0, and 0 is falsy
  return _createErrorResult('INVALID_EXPECTED_CONSUMPTION', ...);
}

// _updateUsageLevel
if (!expectedConsumption) {        // cannot tell 0 from undefined
  return undefined;
}

Reproduction

A feature whose expression involves one limit, evaluated with simple: false:

usage level expectedConsumption expected before this PR
5 { maxPets: 0 } used: 5, no error INVALID_EXPECTED_CONSUMPTION
0 { maxPets: 0 } used: 0, no error INVALID_EXPECTED_CONSUMPTION
0 { maxPets: 1 } used: 1, no error ok (only because 0 + 1 is truthy)

The first row is the "check without charging" case — asking whether an action would be allowed. The second is the same request against a contract that has not consumed anything yet. Neither has anything to do with a missing value, which is what the caller is told.

This also affects any feature whose expression involves several limits where only some are actually spent: the caller must name them all, and the ones that cost nothing can only be declared as zero.

The change

Two lines in api/src/main/utils/feature-evaluation/featureEvaluation.ts:

  • _updateUsageLevel distinguishes an absent value from zero: expectedConsumption === undefined || expectedConsumption === null instead of !expectedConsumption.
  • _buildSuccessResult checks the returned value for undefined instead of for falsiness, so a computed usage level of 0 is a result, not a failure.

The genuinely-missing case is unchanged: a limit absent from expectedConsumption still yields INVALID_EXPECTED_CONSUMPTION, and the separate throw in evaluateFeature that guards the write path is untouched.

Verification

New file api/src/test/feature-evaluation.zero-consumption.test.ts — 8 tests, driving the public evaluateFeature rather than the private helpers:

✓ src/test/feature-evaluation.zero-consumption.test.ts (8 tests)
  Tests  8 passed (8)

Each of the two source changes was reverted independently to confirm the tests are load-bearing: without the _buildSuccessResult fix, 2 of the 8 fail; without the _updateUsageLevel fix, 3 fail.

The existing suite for this module is unaffected:

✓ src/test/feature-evaluation.test.ts (26 tests)
  Tests  26 passed (26)

npx tsc --noEmit is clean.

Scope

Two lines plus a test file. No API shape, no schema and no persisted data changes; a request that worked before still works, and the newly accepted values previously produced an error rather than a different result — so nothing that currently succeeds changes behaviour.

Two falsy checks treated a numeric zero as an absent value, so a caller who
named a limit and declared it costs nothing was told they had not named it
at all. The same check on the sum also refused an ordinary positive
consumption whenever the usage level was still zero, which is the state of
every contract on its first call and after every renewal.
@javiercavlop

Copy link
Copy Markdown
Contributor Author

The red Integration Tests Run check on this PR is not caused by the change. The job reads its Mongo port and database name from the testing environment, which GitHub withholds from pull requests opened from a fork, so the action is asked to publish port `` and docker refuses -p : before any test runs:

docker: invalid publish opts format (should be name=value but got ':').
Error starting MongoDB Docker container

The workflow has never passed on a fork PR — every green run in its history came from a branch inside the repository. #62 fixes it, and its own check is green, which is the fix running on a fork PR. Once that lands this check should go green here too.

@javiercavlop

Copy link
Copy Markdown
Contributor Author

Full suite, verified locally. Since the workflow cannot run on a fork PR until #62 lands, I reproduced the CI environment (Mongo 7.0.16 on 27017, Redis 7 on 6379, an api/.env matching what the workflow generates) and ran every test file in its own vitest process, as run-tests.sh does:

13 files passed, 0 failed

That is the repository's 12 files plus the one this PR adds. The same run on main gives 12 files / 701 tests, all passing, so this branch adds tests and breaks none.

One note on method, since it changed a conclusion: an earlier run of mine reported failures in contract.test.ts and service.test.ts. Those were 5000 ms timeouts caused by my own machine being busy, not by any change here — repeated on an idle machine they pass (77/77 in 124 s, 44/44 in 46 s). One of them also draws a random pricing file per run, so it is flaky by construction.

@Alex-GF
Alex-GF changed the base branch from main to develop July 31, 2026 08:07
@Alex-GF
Alex-GF merged commit 54eed39 into isa-group:develop Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants